JavaScript 基本数据类型详解

JavaScript

03/21/2021


数据类型

数据类型

1. null 和 undefined

null 与 undefined 都可以表示“没有”,含义非常相似。将一个变量赋值为 undefined 或 null,老实说,语法效果几乎没区别。

JS
var a = undefined
// 或者
var a = null

上面代码中,变量 a 分别被赋值为 undefined 和 null,这两种写法的效果几乎等价。

在 if 语句中,它们都会被自动转为 false,相等运算符(==)甚至直接报告两者相等。

JS
if (!undefined) {
console.log("undefined is false")
}
// undefined is false
if (!null) {
console.log("null is false")
}
// null is false
undefined == null
// true

从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有 null,没有 undefined!

既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。

1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 表示"无"。根据 C 语言的传统,null 可以自动转为 0。

JS
Number(null) // 0
5 + null // 5

上面代码中,null 转为数字时,自动变成 0。

但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null 就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果 null 自动转为 0,很不容易发现错误。

因此,他又设计了一个 undefined。区别是这样的:null 是一个表示“空”的对象,转为数值时为 0;undefined 是一个表示"此处无定义"的原始值,转为数值时为 NaN。

JS
Number(undefined) // NaN
5 + undefined // NaN

2. 数值精度

2.1 JS 中 Number

根据国际标准 IEEE 754,JavaScript 浮点数的 64 个二进制位,从最左边开始,是这样组成的:

  • 第 1 位:符号位,0 表示正数,1 表示负数
  • 第 2 位到第 12 位(共 11 位):指数部分
  • 第 13 位到第 64 位(共 52 位):小数部分(即有效数字)

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

指数部分一共有 11 个二进制位,因此大小范围就是 0 到 2047。IEEE 754 规定,如果指数部分的值在 0 到 2047 之间(不含两个端点),那么有效数字的第一位默认总是 1,不保存在 64 位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在 64 位浮点数之中,最长可能为 52 位。因此,JavaScript 提供的有效数字最长为 53 个二进制位。

TEXT
(-1)^符号位 * 1.xx...xx * 2^(指数部分-1023)

上面公式是正常情况下(指数部分在 0 到 2047 之间),一个数在 JavaScript 内部实际的表示形式。

下面有 SOF 一个答案里更为详细的描述:

  1. JavaScript uses double (IEEE 754) to represent all numbers
TEXT
2. `double` consists of [sign, exponent(11bit), mantissa(52bit)] fields. Value of number is computed using formula `(-1)^sign * (1.mantissa) * 2^(exponent - 1023)`. (`1.mantissa` - means that we take bits of mantissa add 1 at the beginning and tread that value as number, e.g. if mantissa = `101` we get number `1.101 (bin) = 1 + 1/2 + 1/8 (dec) = 1.625 (dec)`.
3. We can get value of `sign` bit testing if number is greater than zero. There is a small issue with `0` here because `double` have `+0` and `-0` values, but we can distinguish these two by computing `1/value` and checking if value is `+Inf` or `-Inf`.
4. Since `1 <= 1.mantissa < 2` we can get value of exponent using `Math.log2` e.g. `Math.floor(Math.log2(666.0)) = 9` so exponent is `exponent - 1023 = 9` and `exponent = 1032`, which in binary is `(1032).toString(2) = "10000001000"`
5. After we get exponent we can scale number to zero exponent without changing mantissa, `value = value / Math.pow(2, Math.floor(Math.log2(666.0)))`, now value represents number `(-1)^sign * (1.mantissa)`. If we ignore sign and multiply that by 2^52 we get integer value that have same bits as 1.mantissa: `((666 / Math.pow(2, Math.floor(Math.log2(666)))) * Math.pow(2, 52)).toString(2) = "10100110100000000000000000000000000000000000000000000"` (we must ignore leading 1).
6. After some string concat's you will get what you want

精度最多只能到 53 个二进制位,这意味着,绝对值小于 2 的 53 次方的整数,即253-2^{53}2532^{53},都可以精确表示。

关于 IEEE 754 的详细内容,可以查看这篇

JS
Math.pow(2, 53)
// 9007199254740992
Math.pow(2, 53) + 1
// 9007199254740992
Math.pow(2, 53) + 2
// 9007199254740994
Math.pow(2, 53) + 3
// 9007199254740996
Math.pow(2, 53) + 4
// 9007199254740996

上面代码中,大于 2 的 53 次方以后,整数运算的结果开始出现错误。所以,大于 2 的 53 次方的数值,都无法保持精度。由于 2 的 53 次方是一个 16 位的十进制数值,所以简单的法则就是,JavaScript 对 15 位的十进制数都可以精确处理。

JS
Math.pow(2, 53)
// 9007199254740992
// 多出的三个有效数字,将无法保存
9007199254740992111
// 9007199254740992000

上面示例表明,大于 2 的 53 次方以后,多出来的有效数字(最后三位的 111)都会无法保存,变成 0。

根据标准,64 位浮点数的指数部分的长度是 11 个二进制位,意味着指数部分的最大值是 2047(2 的 11 次方减 1)。也就是说,64 位浮点数的指数部分的值最大为 2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为210242^{1024}210232^{-1023}(开区间),超出这个范围的数无法表示。

如果一个数大于等于 2 的 1024 次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回 Infinity。

JS
Math.pow(2, 1024) // Infinity

如果一个数小于等于 2 的-1075 次方(指数部分最小值-1023,再加上小数部分的 52 位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回 0。

JS
Math.pow(2, -1075) // 0

下面是一个实际的例子

JS
var x = 0.5
for (var i = 0; i < 25; i++) {
x = x * x
}
x // 0

上面代码中,对 0.5 连续做 25 次平方,由于最后结果太接近 0,超出了可表示的范围,JavaScript 就直接将其转为 0。

JavaScript 提供 Number 对象的 MAX_VALUE 和 MIN_VALUE 属性,返回可以表示的具体的最大值和最小值。

JS
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如 35(十进制)和 0xFF(十六进制)。

数值也可以采用科学计数法表示,下面是几个科学计数法的例子。

JS
123e3 // 123000
123e-3 - // 0.123
3.1e12
0.1e-23

科学计数法允许字母eE的后面,跟着一个整数,表示这个数值的指数部分。

以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。

(1)小数点前的数字多于 21 位。

JS
1234567890123456789012
// 1.2345678901234568e+21
123456789012345678901
// 123456789012345680000

(2)小数点后的零多于 5 个。

JS
// 小数点后紧跟5个以上的零,
// 就自动转为科学计数法
0.0000003 // 3e-7
// 否则,就保持原来的字面形式
0.000003 // 0.000003

2.2 进制转换

使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。

十进制:没有前导 0 的数值。 八进制:有前缀 0o 或 0O 的数值,或者有前导 0、且只用到 0-7 的八个阿拉伯数字的数值。 十六进制:有前缀 0x 或 0X 的数值。 二进制:有前缀 0b 或 0B 的数值。 默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。

JS
0xff // 255
0o377 // 255
0b11 // 3

如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。

JS
0xzz // 报错
0o88 // 报错
0b22 // 报错

通常来说,有前导 0 的数值会被视为八进制,但是如果前导 0 后面有数字89,则该数值被视为十进制。

JS
0888 // 888
0777 // 511

前导 0 表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法。

另外,如果想查看整数的二进制表示,可以通过一下方式实现

JS
function int2bin(int) {
return (int >>> 0).toString(2)
}

2.3 特殊数值

  1. 0

JavaScript 内部实际上存在 2 个 0:一个是+0,一个是-0,区别就是 64 位浮点数表示法的符号位不同。它们是等价的。

JS
;-0 === +0 // true
0 === -0 // true
0 ===
+0 + // true
0 - // 0
0(
// 0
-0
)
.toString()(
// '0'
+0
)
.toString() // '0'

唯一有区别的场合是,+0 或-0 当作分母,返回的值是不相等的

JS
1 / +0 === 1 / -0 // false

上面的代码之所以出现这样结果,是因为除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的。

  1. NaN

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。例如

JS
5 - "x" // NaN
Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN

需要注意的是,NaN 不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于 Number,使用 typeof 运算符可以看得很清楚。

JS
typeof NaN // 'number'

NaN 不等于任何值,包括它本身。

JS
NaN === NaN // false

NaN 与任何数(包括它自己)的运算,得到的都是 NaN。

JS
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
NaN * NaN // NaN
  1. Infinity

Infinity 与 NaN 比较,总是返回 false。

JS
Infinity >
NaN - // false
Infinity >
NaN // false
Infinity <
NaN - // false
Infinity <
NaN // false

0 乘以 Infinity,返回 NaN;0 除以 Infinity,返回 0;Infinity 除以 0,返回 Infinity。

JS
0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity

Infinity 加上或乘以 Infinity,返回的还是 Infinity。但是减去或除去 Infinity,得到 NaN

JS
Infinity + Infinity // Infinity
Infinity * Infinity // Infinity
Infinity - Infinity // NaN
Infinity / Infinity // NaN

Infinity 与 null 计算时,null 会转成 0,等同于与 0 的计算。

JS
null * Infinity // NaN
null / Infinity // 0
Infinity / null // Infinity

Infinity 与 undefined 计算,返回的都是 NaN。

JS
undefined + Infinity // NaN
undefined - Infinity // NaN
undefined * Infinity // NaN
undefined / Infinity // NaN
Infinity / undefined // NaN

2.4 parseInt

字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。

JS
parseInt("8a") // 8
parseInt("12**") // 12
parseInt("12.34") // 12
parseInt("15e2") // 15
parseInt("15px") // 15

如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN

JS
parseInt("abc") // NaN
parseInt(".3") // NaN
parseInt("") // NaN
parseInt("+") // NaN
parseInt("+1") // 1

如果字符串以 0x 或 0X 开头,parseInt 会将其按照十六进制数解析。

JS
parseInt("0x10") // 16
parseInt("011") // 11

对于那些会自动转为科学计数法的数字,parseInt 会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。

JS
parseInt(1000000000000000000000.5) // 1
// 等同于
parseInt("1e+21") // 1
parseInt(0.0000008) // 8
// 等同于
parseInt("8e-7") // 8

parseInt 方法还可以接受第二个参数(2 到 36 之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt 的第二个参数为 10,即默认是十进制转十进制。

JS
parseInt("1000") // 1000
// 等同于
parseInt("1000", 10) // 1000
parseInt("1000", 2) // 8
parseInt("1000", 6) // 216
parseInt("1000", 8) // 512

如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在 2 到 36 之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0undefinednull,则直接忽略。

JS
parseInt("10", 37) // NaN
parseInt("10", 1) // NaN
parseInt("10", 0) // 10
parseInt("10", null) // 10
parseInt("10", undefined) // 10

如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回 NaN。

JS
parseInt("1546", 2) // 1
parseInt("546", 2) // NaN

前面说过,如果 parseInt 的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。

JS
parseInt(0x11, 36) // 43
parseInt(0x11, 2) // 1
// 等同于
parseInt(String(0x11), 36)
parseInt(String(0x11), 2)
// 等同于
parseInt("17", 36)
parseInt("17", 2)

上面代码中,十六进制的 0x11 会被先转为十进制的 17,再转为字符串。然后,再用 36 进制或二进制解读字符串 17,最后返回结果 43 和 1。

这种处理方式,对于八进制的前缀 0,尤其需要注意。

JS
parseInt(011, 2) // NaN
// 等同于
parseInt(String(011), 2)
// 等同于
parseInt(String(9), 2)

JavaScript 不再允许将带有前缀 0 的数字视为八进制数,而是要求忽略这个 0。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。

最后,parseInt 和 parseFloat 都会讲 falish 的值转换为NaN,不同于 Number 装箱

JS
parseInt(true) // NaN
parseFloat(true) // NaN
Number(true) // 1
parseInt(null) // NaN
parseFloat(null) // NaN
Number(null) // 0
parseInt("") // NaN
parseFloat("") // NaN
Number("") // 0
parseFloat("123.45#") // 123.45
Number("123.45#") // NaN

2.5 isNaN()

isNaN 只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成 NaN,所以最后返回 true,这一点要特别引起注意。也就是说,isNaN 为 true 的值,有可能不是 NaN,而是一个字符串。

JS
isNaN("Hello") // true
// 相当于
isNaN(Number("Hello")) // true
isNaN({}) // true
// 等同于
isNaN(Number({})) // true
isNaN(["xzy"]) // true
// 等同于
isNaN(Number(["xzy"])) // true

但是,对于空数组和只有一个数值成员的数组,isNaN 返回 false。

JS
isNaN([]) // false
isNaN([123]) // false
isNaN(["123"]) // false

上面代码之所以返回 false,原因是这些数组能被 Number 函数转成数值

因此,使用 isNaN 之前,最好判断一下数据类型。

JS
function myIsNaN(value) {
return typeof value === "number" && isNaN(value)
}

判断 NaN 更可靠的方法是,利用 NaN 为唯一不等于自身的值的这个特点,进行判断。

JS
function myIsNaN(value) {
return value !== value
}

2.6 isFinite()

isFinite 方法返回一个布尔值,表示某个值是否为正常的数值。

JS
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

除了Infinity-InfinityNaNundefined这几个值会返回 false,isFinite 对于其他的数值都会返回 true。

3. String 编码相关

每个字符在 JavaScript 内部都是以 16 位(即 2 个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为 16 位长度,即 2 个字节。

但是,UTF-16 有两种长度:对于码点在U+0000U+FFFF之间的字符,长度为 16 位(即 2 个字节);对于码点在U+10000U+10FFFF之间的字符,长度为 32 位(即 4 个字节),而且前两个字节在0xD8000xDBFF之间,后两个字节在0xDC000xDFFF之间。举例来说,码点U+1D306对应的字符为𝌆,它写成 UTF-16 就是0xD834 0xDF06

JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到 U+FFFF,因此两字节足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。但是,JavaScript 的标准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。上一节的那个四字节字符𝌆,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符。

JS
"𝌆".length // 2

总结一下,对于码点在U+10000U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length 属性为 2)。所以处理的时候,必须把这一点考虑在内,也就是说,JavaScript 返回的字符串长度可能是不正确的。

更多资料查看

TEXT
1. [JavaScript’s internal character encoding: UCS-2 or UTF-16](https://mathiasbynens.be/notes/javascript-encoding)
2. [JavaScript has a Unicode problem](https://mathiasbynens.be/notes/javascript-unicode)
3. [UTF8和UCS2](https://blog.csdn.net/a13935302660/article/details/77507809)

有时,文本里面包含一些不可打印的符号,比如 ASCII 码 0 到 31 的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另一个场景是,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。

所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这 64 个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。

JavaScript 原生提供两个 Base64 相关的方法:

  • bota(): 任意值转为 Base64 编码
  • atob(): Base64 编码转为原来的值
JS
var string = "Hello World!"
btoa(string) // "SGVsbG8gV29ybGQh"
atob("SGVsbG8gV29ybGQh") // "Hello World!"

注意,这两个方法不适合非 ASCII 码的字符,会报错。

JS
btoa("你好") // 报错

要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。

JS
function b64Encode(str) {
return btoa(encodeURIComponent(str))
}
function b64Decode(str) {
return decodeURIComponent(atob(str))
}
b64Encode("你好") // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode("JUU0JUJEJUEwJUU1JUE1JUJE") // "你好"

内置对象,原生对象

内置对象,原生对象

JS 中的对象有内置对象(built-in object),有原生对象(native object),还有宿主对象基本类型等等不同的叫法和分类

这些概念经常混淆在一起,对初学者很不友好,即便是一些 JS 老鸟,可能都没能把它们划分得很清楚,下面我以我的理解,一笔概括它们之间的关系:

  1. 原生对象,可以理解为不是由三方代码产生(也就是你自己写的),而是你使用 JS 时默认可以使用的对象
  2. 原生对象即可以是内置的对象,也可以是内置的函数,因为都是直接可以拿到的,不需要添加额外的代码
  3. 内置对象,包含于原生对象中,也就是说所有内置对象也是直接可以使用的
  4. 内置对象代表这些对象仅仅跟 ECMAScript 有关系,跟宿主环境无关
  5. 容易推断,宿主环境就是非内置的,跟宿主环境有关的原生对象
  6. 宿主对象可以分为 DOMBOM
  7. 基本类型,是 ECMAScript 规定的几种基本的元素值类型,不应当当做对象来看待,所以不应该属于原生对象的范畴,即便我们默认就能使用
  8. 每个基本类型对应着一个内置对象,除了nullundefined
  9. 我们之所以可以对基本类型执行对应类型的内置对象的操作(获取属性,执行成员函数等),是因为 js 在我们对基本类型调用这些操作的时候,进行了装箱操作

总而言之,可以将几个对象之间的关系简单理解为:原生对象包含了内置对象宿主对象基本类型不属于原生对象,但和内置对象一一对应

更详细的说明可以参考博客,但是那个整体架构图建议简单看看就好,太精确的划分反而会让问题变复杂,对这几个概念的关系按照我上面所诉去进行理解即可。

再给出一个内置对象的列表

JAVASCRIPT
Array
Boolean
Date
Error
EvalError
Function
Infinity
JSON
Map
Math
NaN
Number
Object
ParallelArray
Promise
Proxy
RegExp
Set
String
Symbol
SyntaxError
Uint32Array
WeakSet
decodeURI
decodeURIComponent()
encodeURI()
encodeURIComponent()
escape()已废弃
eval()
isFinite()
isNaN()
null
parseFloat
parseInt
undefined